// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.IO;
using ILLink.Shared;
using Mono.Cecil;

namespace Mono.Linker.Steps
{
    public class RootAssemblyInput : BaseStep
    {
        readonly string fileName;
        readonly AssemblyRootMode rootMode;

        public RootAssemblyInput(string fileName, AssemblyRootMode rootMode)
        {
            this.fileName = fileName;
            this.rootMode = rootMode;
        }

        protected override void Process()
        {
            AssemblyDefinition? assembly = LoadAssemblyFile();
            if (assembly == null)
                return;

            var di = new DependencyInfo(DependencyKind.RootAssembly, assembly);
            var origin = new MessageOrigin(assembly);

            AssemblyAction action = Context.Annotations.GetAction(assembly);
            switch (action)
            {
                case AssemblyAction.Copy:
                    Annotations.Mark(assembly.MainModule, di, origin);
                    // Mark Step will take care of marking whole assembly
                    return;
                case AssemblyAction.CopyUsed:
                case AssemblyAction.Link:
                    break;
                default:
                    Context.LogError(null, DiagnosticId.RootAssemblyCannotUseAction, assembly.Name.ToString(), action.ToString());
                    return;
            }

            switch (rootMode)
            {
                case AssemblyRootMode.EntryPoint:
                    var ep = assembly.MainModule.EntryPoint;
                    if (ep == null)
                    {
                        Context.LogError(null, DiagnosticId.RootAssemblyDoesNotHaveEntryPoint, assembly.Name.ToString());
                        return;
                    }

                    Annotations.Mark(ep.DeclaringType, di, origin);
                    Annotations.AddPreservedMethod(ep.DeclaringType, ep);
                    Annotations.SetEntryPointAssembly(assembly);
                    break;
                case AssemblyRootMode.VisibleMembers:
                    var preserve_visible = TypePreserveMembers.Visible;
                    if (MarkInternalsVisibleTo(assembly))
                        preserve_visible |= TypePreserveMembers.Internal;

                    MarkAndPreserve(assembly, preserve_visible);
                    break;

                case AssemblyRootMode.Library:
                    var preserve_library = TypePreserveMembers.Visible | TypePreserveMembers.Library;
                    if (MarkInternalsVisibleTo(assembly))
                        preserve_library |= TypePreserveMembers.Internal;

                    MarkAndPreserve(assembly, preserve_library);

                    // Assembly root mode wins over any enabled optimization which
                    // could conflict with library rooting behaviour
                    Context.Optimizations.Disable(
                        CodeOptimizations.Sealer |
                        CodeOptimizations.UnusedTypeChecks |
                        CodeOptimizations.UnreachableBodies |
                        CodeOptimizations.UnusedInterfaces |
                        CodeOptimizations.RemoveDescriptors |
                        CodeOptimizations.RemoveLinkAttributes |
                        CodeOptimizations.RemoveSubstitutions |
                        CodeOptimizations.RemoveDynamicDependencyAttribute |
                        CodeOptimizations.OptimizeTypeHierarchyAnnotations |
                        CodeOptimizations.SubstituteFeatureGuards, assembly.Name.Name);

                    // Enable EventSource special handling
                    Context.DisableEventSourceSpecialHandling = false;

                    // No metadata trimming
                    Context.MetadataTrimming = MetadataTrimming.None;
                    break;
                case AssemblyRootMode.AllMembers:
                    Annotations.SetRootAssembly(assembly);
                    Annotations.Mark(assembly.MainModule, di, origin);
                    return;
            }
        }

        AssemblyDefinition? LoadAssemblyFile()
        {
            AssemblyDefinition? assembly;

            if (File.Exists(fileName))
            {
                assembly = Context.Resolver.GetAssembly(fileName);
                Context.Resolver.CacheAssembly(assembly);
                return assembly;
            }

            //
            // Quirks mode for netcore to support passing ambiguous assembly name
            //
            assembly = Context.TryResolve(fileName);
            if (assembly == null)
                Context.LogError(null, DiagnosticId.RootAssemblyCouldNotBeFound, fileName);

            return assembly;
        }

        void MarkAndPreserve(AssemblyDefinition assembly, TypePreserveMembers preserve)
        {
            var module = assembly.MainModule;
            if (module.HasExportedTypes)
                foreach (var type in module.ExportedTypes)
                    MarkAndPreserve(assembly, type, preserve);

            foreach (var type in module.Types)
                MarkAndPreserve(type, preserve);
        }

        void MarkAndPreserve(TypeDefinition type, TypePreserveMembers preserve)
        {
            TypePreserveMembers preserve_anything = preserve;
            if ((preserve & TypePreserveMembers.Visible) != 0 && !IsTypeVisible(type))
                preserve_anything &= ~TypePreserveMembers.Visible;

            if ((preserve & TypePreserveMembers.Internal) != 0 && IsTypePrivate(type))
                preserve_anything &= ~TypePreserveMembers.Internal;

            // Keep all interfaces and interface members in library mode
            if ((preserve & TypePreserveMembers.Library) != 0 && type.IsInterface)
            {
                Annotations.Mark(type, new DependencyInfo(DependencyKind.RootAssembly, type.Module.Assembly), new MessageOrigin(type.Module.Assembly));
                Annotations.SetPreserve(type, TypePreserve.All);
            }

            switch (preserve_anything)
            {
                case 0:
                    return;
                case TypePreserveMembers.Library:
                    //
                    // In library mode private type can have members kept for serialization if
                    // the type is referenced
                    //
                    preserve = preserve_anything;
                    Annotations.SetMembersPreserve(type, preserve);
                    break;
                default:
                    Annotations.Mark(type, new DependencyInfo(DependencyKind.RootAssembly, type.Module.Assembly), new MessageOrigin(type.Module.Assembly));
                    Annotations.SetMembersPreserve(type, preserve);
                    break;
            }

            if (!type.HasNestedTypes)
                return;

            foreach (TypeDefinition nested in type.NestedTypes)
                MarkAndPreserve(nested, preserve);
        }

        void MarkAndPreserve(AssemblyDefinition assembly, ExportedType type, TypePreserveMembers preserve)
        {
            var di = new DependencyInfo(DependencyKind.RootAssembly, assembly);
            var origin = new MessageOrigin(assembly);
            Context.Annotations.Mark(type, di, origin);
            Context.Annotations.Mark(assembly.MainModule, di, origin);
            Annotations.SetMembersPreserve(type, preserve);
        }

        static bool IsTypeVisible(TypeDefinition type)
        {
            return type.IsPublic || type.IsNestedPublic || type.IsNestedFamily || type.IsNestedFamilyOrAssembly;
        }

        static bool IsTypePrivate(TypeDefinition type)
        {
            return type.IsNestedPrivate;
        }

        bool MarkInternalsVisibleTo(AssemblyDefinition assembly)
        {
            foreach (CustomAttribute attribute in assembly.CustomAttributes)
            {
                if (attribute.Constructor.DeclaringType.IsTypeOf("System.Runtime.CompilerServices", "InternalsVisibleToAttribute"))
                {
                    Context.Annotations.Mark(attribute, new DependencyInfo(DependencyKind.RootAssembly, assembly));
                    return true;
                }
            }

            return false;
        }
    }
}
